/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import static android.persist.op.sqlite.SQLLogging.SQL_LOG_TAG;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.persist.PersistException;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;
import android.persist.op.CreateStowage;
import android.persist.op.OpException;
import android.reflect.util.ClassReflectionUtil;
import android.util.Log;

/**
 * {@code SQLiteCreateStowage} is an operation that creates the tables necessary
 * to persist a class. All domestic fields are mapped to a column type using
 * {@link SQLiteType#sqlType(Class)}. The table is created using a
 * "CREATE TABLE 'tablename' ..." statement. Since
 * "CREATE TABLE IF NOT EXISTS 'tablename' ..." is not used you must ensure that
 * this operation is utilized only once for each database/class combination.
 * <p>
 * <h3>Examples</h3>
 * <p>
 * Before data can be persisted properly the underlying SQLite tables must be
 * created. This class performs the work necessary to do that. The recommended
 * way of using this class is from within a {@link SQLiteOpenHelper}. Below are
 * two examples.
 * <H2>Create Tables for A Single Class</H2>
 * <p>
 * The simplest use case for {@code SQLiteCreateStowage} is to create a single
 * table necessary to store data for one class. This example will create a
 * single table for a class called {@code YourClass}. None of the tables
 * required to store foreign reference fields are created:
 * <p>
 * <blockquote>
 * 
 * <pre>
 * private class MyOpenHelper extends SQLiteOpenHelper {
 * 
 *     public OpenHelper(Context context, String name, CursorFactory factory, int version) {
 *         super(context, name, factory, version);
 * 
 *     }
 * 
 *     &#064;Override
 *     public void onCreate(SQLiteDatabase db) {
 * 
 *         SQLiteCreateStowage&lt;YourClass&gt; create = new SQLiteCreateStowage&lt;YourClass&gt;(db,
 *                 YourClass.class);
 * 
 *         // invoking SQLiteCreateStowage.call always returns true. A
 *         // RuntimeException is thrown, however, if the table was not created.
 *         // When this happens it is because either the table already exists or
 *         // YourClass is not properly defined.
 * 
 *         create.call();
 * 
 *     }
 * 
 *     &#064;Override
 *     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 * 
 *     }
 * 
 * }
 * </pre>
 * 
 * </blockquote>
 * <p>
 * You can invoke the {@code SQLiteCreateStowage} anywhere in your application,
 * however you must take care to only use it once. Using it more than once per
 * {@link SQLiteDatabase} will result in an error. For this reason it's
 * recommended to use {@code SQLiteCreateStowage} from within an
 * {@link SQLiteOpenHelper} derived class.
 * <p>
 * <H2>Create Tables For a Class' Foreign References</H2>
 * <p>
 * A foreign reference is any {@link Field} defined in a class that is not a
 * primitive, or otherwise can't be stored directly. In simple terms, a foreign
 * reference is a {@link Field} that has another class as its type.
 * <p>
 * <blockquote>
 * 
 * <pre>
 * public class Soundgarden {
 * 
 *     // a foreign reference - needs its own table.
 *     private Single blackHoleSun;
 * 
 *     // a domestic reference
 *     private Long unitsSold = Long.MAX_VALUE;
 * 
 *     // a domestic reference - because it is directly storable.
 *     private String songLength = &quot;5:18&quot;;
 * 
 * }
 * </pre>
 * 
 * </blockquote>
 * <p>
 * With SQLite as the data store, the foreign reference of a class is stored as
 * an {@link SQLiteType#INTEGER}. The {@code ROWID} of the foreign reference is
 * stored so that it may be found when loading.
 * <p>
 * Instantiating {@code SQLiteCreateStowage} directly and invoking
 * {@code call()} creates a single table for one class. But chances are you also
 * need to create tables for each encapsulated class. Take for example these two
 * class definitions:
 * <p>
 * <blockquote>
 * 
 * <pre>
 * 
 * public class Bar {
 *     &#064;DataStoreField(key = true)
 *     public int id;
 * }
 * 
 * public class Foo {
 *     &#064;DataStoreField(key = true)
 *     public String s;
 *     public boolean b;
 *     public float f;
 *     public int i;
 *     public double d;
 *     public char c;
 *     public byte Byte;
 *     public short Short;
 *     public long l;
 *     public Bar barInstance;
 * 
 * }
 * </pre>
 * 
 * </blockquote>
 * <p>
 * {@code Foo} contains a foreign reference field, {@code Bar}. In order to
 * persist all of {@code Foo}'s data, two tables must be created; one for
 * {@code Foo} and one for {@code Bar}. Fortunately, there is a static function
 * defined in {@code SQLiteCreateStowage} that will help out with creating all
 * the tables that {@code Foo} needs to persist data. The function is called
 * {@link SQLiteCreateStowage#createTable(SQLiteDatabase, Class, ExecutorService)}.
 * <p>
 * <blockquote>
 * 
 * <pre>
 * private class MyOpenHelper extends SQLiteOpenHelper {
 * 
 *     public OpenHelper(Context context, String name, CursorFactory factory, int version) {
 *         super(context, name, factory, version);
 * 
 *     }
 * 
 *     &#064;Override
 *     public void onCreate(SQLiteDatabase db) {
 * 
 *         // create tables for a class and all classes for its fields.
 *         SQLiteCreateStowage.createTable(db, Foo.class, null);
 * 
 *     }
 * 
 *     &#064;Override
 *     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 * 
 *     }
 * 
 * }
 * </pre>
 * 
 * </blockquote>
 * <p>
 * {@code createTable} will create tables for a class any and class that it
 * encapsulates. It will not, however create tables for inner classes unless the
 * inner class is used to define a field in the outer class. The following
 * example shows how to use {@code createTable}.
 * 
 * @author Richard Schilling
 * @param <T> the class type to create a table for.
 * @since 1.0
 * @see SQLiteType#sqlType(Class)
 */
public class SQLiteCreateStowage<OT> extends CreateStowage<OT> {

    /**
     * The database that the table is created in.
     */
    private final SQLiteDatabase mDatabase;

    /**
     * Class configuration object for {@link operand}'s type.
     */
    private final ClassConfig<OT> mConfig;

    /**
     * Creates an instance and checks to be sure the parameters are valid.
     * 
     * @param db the database to create a table in.
     * @param c the class to create a table for.
     * @since 1.0
     * @throws IllegalArgumentException if db or c is null
     * @throws OpException if the database is not writeable.
     */
    public SQLiteCreateStowage(SQLiteDatabase db, Class<OT> c) {
        super(c);

        if (db == null) {
            throw new IllegalArgumentException("db cannot be null");
        }

        if (db.isReadOnly()) {
            throw new OpException("database must be writeable");
        }

        if (c == null) {
            throw new IllegalArgumentException("c cannot be null");
        }

        if (Collection.class.isAssignableFrom(c)) {
            throw new IllegalArgumentException("collections cannot have their own tables.");
        }

        mDatabase = db;
        mConfig = (ClassConfig<OT>) ClassConfig.getConfig(operand);

    }

    /**
     * Creates the table needed to persist class types passed to the
     * constructor. Always returns true. If the creation fails a runtime
     * exception is thrown
     * 
     * @author Richard Schilling
     * @since 1.0
     * @return true
     * @throws SQLiteException if the SQL operation fails
     * @throws OpException if the database is closed.
     */
    @Override
    public Boolean call() {
        if (!mDatabase.isOpen()) {
            throw new OpException("database is closed");
        }

        createSchema();
        return true;

    }

    /**
     * Does the work of executing SQL statements to create the table.
     * 
     * @since 1.0
     */
    private void createSchema() {

        createTable();
        createCollectionTables();
        createPrimaryKey();
        createIndexes();
        createCollectionIndexes();
    }

    /**
     * Runs the SQL statement necessary to create the table to persist operand's
     * data in.
     * 
     * @since 1.0
     */
    private void createTable() {

        StringBuilder query = new StringBuilder();
        query.append("CREATE TABLE ");
        query.append(mConfig.storageName);
        query.append(" (");

        for (int outer = 0; outer < mConfig.allFields.size(); outer++) {
            FieldConfig fc = mConfig.allFields.get(outer);

            query.append(" ");
            query.append(fc.getPersistedName());
            query.append(" ");

            if (fc.isDomestic()) {

                query.append(SQLiteType.sqlType(fc.field.getType()));

            } else {

                query.append(SQLiteType.INTEGER);
            }

            if (outer < mConfig.allFields.size() - 1) {
                query.append(", ");
            }

        }

        query.append(")");

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    query.toString());
        }
        mDatabase.execSQL(query.toString());

    }

    private void createCollectionTables() {
        for (FieldConfig fConfig : mConfig.foreignFields) {
            // parenttable_fieldname
            // parent column: storage name of the parent class.

            if (!fConfig.isCollection()) {
                continue;
            }

            StringBuilder query = new StringBuilder();
            query.append("CREATE TABLE ");
            query.append(mConfig.storageName);
            query.append("_");
            query.append(fConfig.getPersistedName());
            query.append(" (");
            query.append("parent_ROWID ");
            query.append(SQLiteType.INTEGER.toString());
            query.append(", ");
            query.append(fConfig.getPersistedName());
            query.append(" ");

            /*
             * create a table for the collection values.
             */

            Class<?> columnTypeClass = fConfig.getCollectionType();
            if (ClassReflectionUtil.isPrimitiveOrArray(columnTypeClass)) {
                // child column: primitive SQL type
                query.append(SQLiteType.sqlType(columnTypeClass));
            } else {
                // child column: integer to contain a long value
                query.append(SQLiteType.INTEGER.toString());
            }

            query.append(")");

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        query.toString());
            }
            mDatabase.execSQL(query.toString());

        }
    }

    /**
     * Runs the SQL statements needed to create the primary key. All the fields
     * annotated with {@code DataStoreField.key=true} become part of the
     * compound key.
     * 
     * @since 1.0
     */
    private void createPrimaryKey() {

        StringBuilder query = new StringBuilder();
        query.append("CREATE UNIQUE INDEX 'pk_");
        query.append(mConfig.storageName);
        query.append("' ON '");
        query.append(mConfig.storageName);
        query.append("' (");

        List<String> keyNames = mConfig.getKeyNames();
        int sz = keyNames.size();
        for (int i = 0; i < sz; i++) {

            query.append(keyNames.get(i));
            query.append(" ASC");
            if (i < sz - 1)
                query.append(", ");
        }
        query.append(")");

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    query.toString());
        }
        mDatabase.execSQL(query.toString());
    }

    /**
     * Executes the SQL statements necessary to create indexes. Foreign
     * reference fields are created as INTEGER columns. Indexes are created for
     * every field with an annotation of DataStoreField.index=true.
     * 
     * @since 1.0
     */
    private void createIndexes() {

        // add indexes in this class
        for (FieldConfig fc : mConfig.allFields) {

            /*
             * add an index for all fields that have the annotation set.
             */
            if (fc.annotation != null && fc.annotation.index()) {
                createFieldIndex(fc);
            }

            if (!fc.isDomestic()) {
                createForeignKey(fc);
            }

        }

    }

    private void createCollectionIndexes() {

        for (FieldConfig fConfig : mConfig.foreignFields) {
            // parenttable_fieldname
            // parent column: storage name of the parent class.

            if (!fConfig.isCollection()) {
                continue;
            }
            StringBuilder tableName = new StringBuilder();
            tableName.append(mConfig.storageName);
            tableName.append("_");
            tableName.append(fConfig.getPersistedName());

            StringBuilder query = new StringBuilder();
            query.append("CREATE INDEX 'idx_");
            query.append(tableName);
            query.append("' ON '");
            query.append(tableName);
            query.append("' (parent_ROWID ASC)");

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        query.toString());
            }
            mDatabase.execSQL(query.toString());

        }

    }

    /**
     * Creates an index for a field.
     * 
     * @param fConfig the field to create the index for.
     * @since 1.0
     */
    private void createFieldIndex(FieldConfig fConfig) {

        StringBuilder query = new StringBuilder();

        query.append("CREATE ");

        if (fConfig.annotation.unique())
            query.append("UNIQUE ");

        query.append("INDEX 'idx_");
        query.append(fConfig.getPersistedName());
        query.append("' ON '");
        query.append(mConfig.storageName);
        query.append("' ('");
        query.append(fConfig.getPersistedName());
        query.append("' ASC)");

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    query.toString());
        }
        mDatabase.execSQL(query.toString());

    }

    /**
     * Creates an index for a foreign key.
     * 
     * @param fConfig the field to create an index for.
     * @since 1.0
     */
    private void createForeignKey(
            FieldConfig fConfig) {

        /*
         * add a foreign key index that includes all keys from the foreign
         * object.
         */
        @SuppressWarnings({
                "rawtypes", "unchecked"
        })
        StringBuilder query = new StringBuilder();
        query.append("CREATE INDEX 'fk_");
        query.append(mConfig.storageName);
        query.append("_");
        query.append(fConfig.getPersistedName());
        query.append("' ON '");
        query.append(mConfig.storageName);
        query.append("' (");
        query.append(fConfig.getPersistedName());
        query.append(" ASC");
        query.append(")");

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    query.toString());
        }
        mDatabase.execSQL(query.toString());
    }

    /**
     * Create a list of tasks that will create tables for a class and any
     * classes of foreign reference fields.
     * 
     * @param forClass the class to operate on which will also become the
     *            operand of the first item in the returned list.
     * @return a list of operations that when run will create storage for all
     *         the classes that are needed for persisting data.
     * @since 1.0
     */
    @SuppressWarnings({
            "rawtypes", "unchecked"
    })
    public static List<SQLiteCreateStowage<?>> generateOperations(SQLiteDatabase db,
            Class<?> forClass) {

        ClassConfig<?> config = ClassConfig.getConfig(forClass);

        List<SQLiteCreateStowage<?>> result = new ArrayList<SQLiteCreateStowage<?>>();

        result.add(new SQLiteCreateStowage(db, forClass));

        for (FieldConfig fConfig : config.foreignFields) {

            if (fConfig.isCollection()) {
                continue;
            }
            boolean found = false;
            Class<?> fType = fConfig.field.getType();
            // make sure the table isn't already in result.
            for (SQLiteCreateStowage op : result) {
                if ((Class<?>) op.operand == fType) {
                    found = true;
                    break;
                }

            }
            if (!found) {
                result.add(new SQLiteCreateStowage(db, fType));
            }
        }
        return result;

    }

    /**
     * Creates the underlying table for the specified class as well as the
     * tables for the classes required by any of the fields. This is
     * accomplished by generating multiple SQLiteCreateStorage operations and
     * running them. One SQLiteCreateStorage operation per class is executed.
     * 
     * @param db the database to add tables to.
     * @param c the class to create tables for
     * @param executor if not null then the executor will be used to create
     *            tables and this call will block until all tables are created
     * @since 1.0
     */
    public static void createTable(SQLiteDatabase db, Class<?> c, ExecutorService executor) {

        // get all the operations
        List<SQLiteCreateStowage<?>> ops = generateOperations(db,
                c);

        if (executor == null) {
            // run on the current thread
            for (SQLiteCreateStowage<?> op : ops) {
                // for SQLiteCreateStowage the result of call is always true
                op.call();
            }

            return;

        }

        List<Future<Boolean>> results = new LinkedList<Future<Boolean>>();

        for (SQLiteCreateStowage<?> op : ops) {
            results.add(executor.submit(op));
        }

        for (Future<Boolean> result : results) {
            try {
                if (!result.get()) {
                    throw new PersistException("a create table operation failed");
                }
            } catch (InterruptedException e) {
                throw new PersistException("interrupted", e);
            } catch (ExecutionException e) {
                throw new PersistException("unable to complete operation", e);
            }

        }

    }

}
